/*******************************************************************************
 * Copyright (c) 2000, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.jsdt.internal.corext.refactoring.code.flow;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.wst.jsdt.core.dom.CatchClause;
import org.eclipse.wst.jsdt.core.dom.ITypeBinding;
import org.eclipse.wst.jsdt.core.dom.IVariableBinding;
import org.eclipse.wst.jsdt.core.dom.SimpleName;
import org.eclipse.wst.jsdt.core.dom.TryStatement;

public abstract class FlowInfo {
	
	// Return statement handling.
	protected static final int NOT_POSSIBLE=	0;
	protected static final int UNDEFINED= 		1;
	protected static final int NO_RETURN= 		2;
	protected static final int PARTIAL_RETURN= 	3;
	protected static final int VOID_RETURN= 	4;
	protected static final int VALUE_RETURN=	5;
	protected static final int THROW=			6;

	// Local access handling.
	public static final int UNUSED=				1 << 0;
	public static final int READ= 				1 << 1;
	public static final int READ_POTENTIAL=		1 << 2;
	public static final int WRITE= 				1 << 3;
	public static final int WRITE_POTENTIAL=    1 << 4;
	public static final int UNKNOWN= 			1 << 5;
	
	// Table to merge access modes for condition statements (e.g branch[x] || branch[y]). 
	private static final int[][] ACCESS_MODE_CONDITIONAL_TABLE= {
	/*	  					  UNUSED		   READ			    READ_POTENTIAL   WRTIE			  WRITE_POTENTIAL  UNKNOWN */
	/* UNUSED */			{ UNUSED,		   READ_POTENTIAL,  READ_POTENTIAL,  WRITE_POTENTIAL, WRITE_POTENTIAL, UNKNOWN },
	/* READ */				{ READ_POTENTIAL,  READ,			READ_POTENTIAL,  UNKNOWN,		  UNKNOWN,         UNKNOWN },
	/* READ_POTENTIAL */	{ READ_POTENTIAL,  READ_POTENTIAL,  READ_POTENTIAL,  UNKNOWN,		  UNKNOWN,         UNKNOWN },
	/* WRITE */				{ WRITE_POTENTIAL, UNKNOWN,			UNKNOWN,		 WRITE,			  WRITE_POTENTIAL, UNKNOWN },
	/* WRITE_POTENTIAL */   { WRITE_POTENTIAL, UNKNOWN,			UNKNOWN,		 WRITE_POTENTIAL, WRITE_POTENTIAL, UNKNOWN },
	/* UNKNOWN */ 			{ UNKNOWN, 		   UNKNOWN,			UNKNOWN,		 UNKNOWN, 		  UNKNOWN,		   UNKNOWN }
	};
	
	// Table to change access mode if there is an open branch statement
	private static final int[] ACCESS_MODE_OPEN_BRANCH_TABLE= {
	/*	UNUSED	READ			READ_POTENTIAL  WRTIE				WRITE_POTENTIAL  UNKNOWN */
		UNUSED,	READ_POTENTIAL,	READ_POTENTIAL,	WRITE_POTENTIAL,	WRITE_POTENTIAL, UNKNOWN
	};
	
	// Table to merge return modes for condition statements (y: fReturnKind, x: other.fReturnKind)
	private static final int[][] RETURN_KIND_CONDITIONAL_TABLE = {
	/* 						  NOT_POSSIBLE		UNDEFINED		NO_RETURN		PARTIAL_RETURN	VOID_RETURN		VALUE_RETURN	THROW */
	/* NOT_POSSIBLE */		{ NOT_POSSIBLE,		NOT_POSSIBLE,	NOT_POSSIBLE, 	NOT_POSSIBLE,	NOT_POSSIBLE,	NOT_POSSIBLE,	NOT_POSSIBLE	},
	/* UNDEFINED */			{ NOT_POSSIBLE,		UNDEFINED,		NO_RETURN,		PARTIAL_RETURN, VOID_RETURN,	VALUE_RETURN,	THROW 			},	
	/* NO_RETURN */			{ NOT_POSSIBLE,		NO_RETURN,		NO_RETURN,		PARTIAL_RETURN,	PARTIAL_RETURN, PARTIAL_RETURN, NO_RETURN 		},
	/* PARTIAL_RETURN */	{ NOT_POSSIBLE,		PARTIAL_RETURN,	PARTIAL_RETURN,	PARTIAL_RETURN, PARTIAL_RETURN, PARTIAL_RETURN, PARTIAL_RETURN	},
	/* VOID_RETURN */		{ NOT_POSSIBLE,		VOID_RETURN,	PARTIAL_RETURN,	PARTIAL_RETURN, VOID_RETURN,	NOT_POSSIBLE,	VOID_RETURN		},
	/* VALUE_RETURN */		{ NOT_POSSIBLE,		VALUE_RETURN,	PARTIAL_RETURN, PARTIAL_RETURN, NOT_POSSIBLE,	VALUE_RETURN,	VALUE_RETURN	},
	/* THROW */				{ NOT_POSSIBLE,		THROW,			NO_RETURN,		PARTIAL_RETURN, VOID_RETURN,	VALUE_RETURN,	THROW			}
	};
		
	// Table to merge return modes for sequential statements (y: fReturnKind, x: other.fReturnKind)
	private static final int[][] RETURN_KIND_SEQUENTIAL_TABLE = {
	/* 						  NOT_POSSIBLE		UNDEFINED		NO_RETURN		PARTIAL_RETURN	VOID_RETURN		VALUE_RETURN	THROW */
	/* NOT_POSSIBLE */		{ NOT_POSSIBLE,		NOT_POSSIBLE,	NOT_POSSIBLE, 	NOT_POSSIBLE,	NOT_POSSIBLE,	NOT_POSSIBLE,	NOT_POSSIBLE	},
	/* UNDEFINED */			{ NOT_POSSIBLE,		UNDEFINED,		NO_RETURN,		PARTIAL_RETURN,	VOID_RETURN,	VALUE_RETURN,	THROW			},
	/* NO_RETURN */			{ NOT_POSSIBLE,		NO_RETURN,		NO_RETURN,		PARTIAL_RETURN,	VOID_RETURN,	VALUE_RETURN,	THROW			},
	/* PARTIAL_RETURN */	{ NOT_POSSIBLE,		PARTIAL_RETURN,	PARTIAL_RETURN,	PARTIAL_RETURN,	VOID_RETURN,	VALUE_RETURN,	THROW			},
	/* VOID_RETURN */		{ NOT_POSSIBLE,		VOID_RETURN,	VOID_RETURN,	PARTIAL_RETURN,	VOID_RETURN,	NOT_POSSIBLE,	NOT_POSSIBLE	},
	/* VALUE_RETURN */		{ NOT_POSSIBLE,		VALUE_RETURN,	VALUE_RETURN,	PARTIAL_RETURN,	NOT_POSSIBLE,	VALUE_RETURN,	NOT_POSSIBLE	},
	/* THROW */				{ NOT_POSSIBLE,		THROW,			THROW,			PARTIAL_RETURN,	VOID_RETURN,	VALUE_RETURN,	THROW			}
	};
		
	protected static final String UNLABELED = "@unlabeled"; //$NON-NLS-1$
	protected static final IVariableBinding[] EMPTY_ARRAY= new IVariableBinding[0];

	protected int fReturnKind;
	protected int[] fAccessModes;
	protected Set fBranches;
	protected Set fExceptions;
	protected Set fTypeVariables;
	
	protected FlowInfo() {
		this(UNDEFINED);
	}
	
	protected FlowInfo(int returnKind) {
		fReturnKind= returnKind;
	}
	
	//---- General Helpers ----------------------------------------------------------
	
	protected void assignExecutionFlow(FlowInfo right) {
		fReturnKind= right.fReturnKind;
		fBranches= right.fBranches;
		fExceptions= right.fExceptions;
	}
	
	protected void assignAccessMode(FlowInfo right) {
		fAccessModes= right.fAccessModes;
	}
	
	protected void assign(FlowInfo right) {
		assignExecutionFlow(right);
		assignAccessMode(right);
	}
	
	protected void mergeConditional(FlowInfo info, FlowContext context) {
		mergeAccessModeConditional(info, context);
		mergeExecutionFlowConditional(info, context);
		mergeTypeVariablesConditional(info, context);
	}
	
	protected void mergeSequential(FlowInfo info, FlowContext context) {
		mergeAccessModeSequential(info, context);
		mergeExecutionFlowSequential(info, context);
		mergeTypeVariablesSequential(info, context);
	}
	
	//---- Return Kind ------------------------------------------------------------------
	
	public void setNoReturn() {
		fReturnKind= NO_RETURN;
	}
	
	public boolean isUndefined() {
		return fReturnKind == UNDEFINED;
	}
	
	public boolean isNoReturn() {
		return fReturnKind == NO_RETURN;
	}
	
	public boolean isPartialReturn() {
		return fReturnKind == PARTIAL_RETURN;
	}
	
	public boolean isVoidReturn() {
		return fReturnKind == VOID_RETURN;
	}
	
	public boolean isValueReturn() {
		return fReturnKind == VALUE_RETURN;
	}
	
	public boolean isThrow() {
		return fReturnKind == THROW;
	}

	public boolean isReturn() {
		return (fReturnKind == VOID_RETURN) || (fReturnKind == VALUE_RETURN);
	}
	
	//---- Branches -------------------------------------------------------------------------
	
	public boolean branches() {
		return (fBranches != null) && !fBranches.isEmpty();
	}
	
	protected Set getBranches() {
		return fBranches;
	}
	
	protected void removeLabel(SimpleName label) {
		if (fBranches != null) {
			fBranches.remove(makeString(label));
			if (fBranches.isEmpty()) {
				fBranches= null;
			}
		}
	}
	
	protected static String makeString(SimpleName label) {
		if (label == null) {
			return UNLABELED;
		} else {
			return label.getIdentifier();
		}
	}
	
	//---- Exceptions -----------------------------------------------------------------------
	
	public ITypeBinding[] getExceptions() {
		if (fExceptions == null) {
			return new ITypeBinding[0];
		}
		return (ITypeBinding[]) fExceptions.toArray(new ITypeBinding[fExceptions.size()]);
	}

	protected boolean hasUncaughtException() {
		return (fExceptions != null) && !fExceptions.isEmpty();
	}
	
	protected void addException(ITypeBinding type) {
		if (fExceptions == null) {
			fExceptions= new HashSet(2);
		}
		fExceptions.add(type);
	}
	
	protected void removeExceptions(TryStatement node) {
		if (fExceptions == null) {
			return;
		}
			
		List catchClauses= node.catchClauses();
		if (catchClauses.isEmpty()) {
			return;
		}
		// Make sure we have a copy since we are modifing the fExceptions list	
		ITypeBinding[] exceptions= (ITypeBinding[]) fExceptions.toArray(new ITypeBinding[fExceptions.size()]);
		for (int i= 0; i < exceptions.length; i++) {
			handleException(catchClauses, exceptions[i]);
		}
		if (fExceptions.isEmpty()) {
			fExceptions= null;
		}
	}

	private void handleException(List catchClauses, ITypeBinding type) {
		for (Iterator iter= catchClauses.iterator(); iter.hasNext();) {
			IVariableBinding binding= ((CatchClause)iter.next()).getException().resolveBinding();
			if (binding == null) {
				continue;
			}
			ITypeBinding catchedType= binding.getType();
			while (catchedType != null) {
				if (catchedType == type) {
					fExceptions.remove(type);
					return;
				}
				catchedType= catchedType.getSuperclass();
			}
		}
	}
	
	//---- Type parameters -----------------------------------------------------------------
	
	public ITypeBinding[] getTypeVariables() {
		if (fTypeVariables == null) {
			return new ITypeBinding[0];
		}
		return (ITypeBinding[])fTypeVariables.toArray(new ITypeBinding[fTypeVariables.size()]);
	}
	
	protected void addTypeVariable(ITypeBinding typeParameter) {
		if (fTypeVariables == null) {
			fTypeVariables= new HashSet();
		}
		fTypeVariables.add(typeParameter);
	}
	
	private void mergeTypeVariablesSequential(FlowInfo otherInfo, FlowContext context) {
		fTypeVariables= mergeSets(fTypeVariables, otherInfo.fTypeVariables);
	}
	
	private void mergeTypeVariablesConditional(FlowInfo otherInfo, FlowContext context) {
		fTypeVariables= mergeSets(fTypeVariables, otherInfo.fTypeVariables);
	}
	
	//---- Execution flow -------------------------------------------------------------------
	
	private void mergeExecutionFlowSequential(FlowInfo otherInfo, FlowContext context) {
		int other= otherInfo.fReturnKind;
		if (branches() && (other == VALUE_RETURN)) {
			other= PARTIAL_RETURN;
		}
		fReturnKind= RETURN_KIND_SEQUENTIAL_TABLE[fReturnKind][other];
		mergeBranches(otherInfo, context);
		mergeExceptions(otherInfo, context);
	}
	
	private void mergeExecutionFlowConditional(FlowInfo otherInfo, FlowContext context) {
		fReturnKind= RETURN_KIND_CONDITIONAL_TABLE[fReturnKind][otherInfo.fReturnKind];
		mergeBranches(otherInfo, context);			
		mergeExceptions(otherInfo, context);
	}
	
	private void mergeBranches(FlowInfo otherInfo, FlowContext context) {
		fBranches= mergeSets(fBranches, otherInfo.fBranches);
	}

	private void mergeExceptions(FlowInfo otherInfo, FlowContext context) {
		fExceptions= mergeSets(fExceptions, otherInfo.fExceptions);
	}
	
	private static Set mergeSets(Set thisSet, Set otherSet) {
		if (otherSet != null) {
			if (thisSet == null) {
				thisSet= otherSet;
			} else {
				Iterator iter= otherSet.iterator();
				while (iter.hasNext()) {
					thisSet.add(iter.next());
				}
			}
		}
		return thisSet;
	}
	
	//---- Local access handling --------------------------------------------------
	
	/**
	 * Returns an array of <code>IVariableBinding</code> that conform to the given
	 * access mode <code>mode</code>.
	 * 
	 * @param context the flow context object used to compute this flow info
	 * @param mode the access type. Valid values are <code>READ</code>, <code>WRITE</code>,
	 *  <code>UNKNOWN</code> and any combination of them.
	 * @return an array of local variable bindings conforming to the given type.
	 */
	public IVariableBinding[] get(FlowContext context, int mode) {
		List result= new ArrayList();
		int[] locals= getAccessModes();
		if (locals == null) {
			return EMPTY_ARRAY;
		}
		for (int i= 0; i < locals.length; i++) {
			int accessMode= locals[i];
			if ((accessMode & mode) != 0) {
				result.add(context.getLocalFromIndex(i));
			}
		}
		return (IVariableBinding[])result.toArray(new IVariableBinding[result.size()]);
	}
	
	/**
	 * Checks whether the given local variable binding has the given access
	 * mode.
	 * 
	 * @return <code>true</code> if the binding has the given access mode. 
	 * 	<code>False</code> otherwise
	 */
	public boolean hasAccessMode(FlowContext context, IVariableBinding local, int mode) {
		boolean unusedMode= (mode & UNUSED) != 0;
		if ((fAccessModes == null) && unusedMode) {
			return true;
		}
		int index= context.getIndexFromLocal(local);
		if (index == -1) {
			return unusedMode;
		}
		return (fAccessModes[index] & mode) != 0;
	}
	
	/**
	 * Returns the access mode of the local variable identified by the given binding.
	 * 
	 * @param context the flow context used during flow analysis
	 * @param local the local variable of interest
	 * @return the access mode of the local variable
	 */
	public int getAccessMode(FlowContext context, IVariableBinding local) {
		if (fAccessModes == null) {
			return UNUSED;
		}
		int index= context.getIndexFromLocal(local);
		if (index == -1) {
			return UNUSED;
		}
		return fAccessModes[index];
	}
	
	protected int[] getAccessModes() {
		return fAccessModes;
	}
	
	protected void clearAccessMode(IVariableBinding binding, FlowContext context) {
		if (fAccessModes == null) {
			return;
		}
		fAccessModes[binding.getVariableId() - context.getStartingIndex()]= UNUSED;
	}
	
	protected void mergeAccessModeSequential(FlowInfo otherInfo, FlowContext context) {
		if (!context.considerAccessMode()) {
			return;
		}
		
		int[] others= otherInfo.fAccessModes;
		if (others == null) {
			return;
		}
			
		// Must not consider return kind since a return statement can't control execution flow
		// inside a method. It always leaves the method.	
		if (branches() || hasUncaughtException()) {
			for (int i= 0; i < others.length; i++) {
				others[i]= ACCESS_MODE_OPEN_BRANCH_TABLE[getIndex(others[i])];
			}
		}
		
		if (fAccessModes == null) {	// all current variables are unused
			fAccessModes= others;
			return;
		}
		
		if (context.computeArguments()) {
			handleComputeArguments(others);
		} else if (context.computeReturnValues()) {
			handleComputeReturnValues(others);
		} else if (context.computeMerge()) {
			handleMergeValues(others);
		}
	}

	private void handleComputeReturnValues(int[] others) {
		for (int i= 0; i < fAccessModes.length; i++) {
			int accessmode= fAccessModes[i];
			int othermode= others[i];
			if (accessmode == WRITE) {
				continue;
			}
			if (accessmode == WRITE_POTENTIAL) {
				if (othermode == WRITE) {
					fAccessModes[i]= WRITE;
				}
				continue;
			}
		
			if (others[i] != UNUSED) {
				fAccessModes[i]= othermode;
			}
		}
	}

	private void handleComputeArguments(int[] others) {
		for (int i= 0; i < fAccessModes.length; i++) {
			int accessMode= fAccessModes[i];
			int otherMode= others[i];
			if (accessMode == UNUSED) {
				fAccessModes[i]= otherMode;
			} else if ((accessMode == WRITE_POTENTIAL) && ((otherMode == READ) || (otherMode == READ_POTENTIAL))) {
				// Read always supersedes a potential write even if the read is potential as well
				// (we have to consider the potential read as an argument then).
				fAccessModes[i]= otherMode;
			} else if ((accessMode == WRITE_POTENTIAL) && (otherMode == WRITE)) {
				fAccessModes[i]= WRITE;
			}
		}
	}
	
	private void handleMergeValues(int[] others) {
		for (int i= 0; i < fAccessModes.length; i++) {
			fAccessModes[i]= ACCESS_MODE_CONDITIONAL_TABLE
				[getIndex(fAccessModes[i])]
				[getIndex(others[i])];
		}
	}
	
	protected void createAccessModeArray(FlowContext context) {
		fAccessModes= new int[context.getArrayLength()];
		for (int i= 0; i < fAccessModes.length; i++) {
			fAccessModes[i]= UNUSED;
		}
	}
	
	protected void mergeAccessModeConditional(FlowInfo otherInfo, FlowContext context) {
		if (!context.considerAccessMode()) {
			return;
		}
		
		int[] others= otherInfo.fAccessModes;
		// first access
		if (fAccessModes == null) {
			if (others != null) {
				fAccessModes= others;
			} else {
				createAccessModeArray(context);
			}
			return;
		} else {	
			if (others == null) {
				for (int i= 0; i < fAccessModes.length; i++) {
					int unused_index= getIndex(UNUSED);
					fAccessModes[i]= ACCESS_MODE_CONDITIONAL_TABLE
						[getIndex(fAccessModes[i])]
						[unused_index];
				}
			} else {
				for (int i= 0; i < fAccessModes.length; i++) {
					fAccessModes[i]= ACCESS_MODE_CONDITIONAL_TABLE
						[getIndex(fAccessModes[i])]
						[getIndex(others[i])];
				}
			}
		}
	}
	
	protected void mergeEmptyCondition(FlowContext context) {
		if ((fReturnKind == VALUE_RETURN) || (fReturnKind == VOID_RETURN)) {
			fReturnKind= PARTIAL_RETURN;
		}
			
		if (!context.considerAccessMode()) {
			return;
		}
			
		if (fAccessModes == null) {
			createAccessModeArray(context);
			return;
		}
		
		int unused_index= getIndex(UNUSED);
		for (int i= 0; i < fAccessModes.length; i++) {
			fAccessModes[i]= ACCESS_MODE_CONDITIONAL_TABLE
				[getIndex(fAccessModes[i])]
				[unused_index];
		}
	}
	
	private static int getIndex(int accessMode) {
		 // Fast log function
		 switch (accessMode) {
		 	case UNUSED:
		 		return 0;
		 	case READ:
		 		return 1;
		 	case READ_POTENTIAL:
		 		return 2;
		 	case WRITE:
		 		return 3;
		 	case WRITE_POTENTIAL:
		 		return 4;
		 	case UNKNOWN:
		 		return 5;
		 }
		 return -1;
	}
}


